JavaScript

Aim

To provide a scriptable plugin that allows users to tailor the functioning of OpenCPN without getting into the complexity of writing a plugin.

Complete User Guide
Releases (including installers)
Github Repository
Cruisers Forum Thread

How does it work?

The plugin presents a console window comprising a script pane and a results pane and some buttons. You enter your JavaScript in the script pane (or load it from a file) and click on the Run button. The script is compiled into byte code and executed and any result is displayed. At its simplest, enter, say

(4+8)/3

and the result 4 is displayed. But you could also enter, say

function fib(n) {
    if (n == 0) { return 0; }
    if (n == 1) { return 1; }
    return fib(n-1) + fib(n-2);
}
function build(n) {
    var res = [];
    for (i = 0; i <n; i++) {
        res.push(fib(i));
    }
    return(res.join(' '));
}

print("Fibonacci says: ", build(20), “\n");

Which displays

Fibonacci says: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181

This illustrates how functions can be defined and called, including recursively. So we have a super calculator!

I have been developing interfaces (APIs) to OpenCPN and it is here that the fun starts. As an example, the following statement takes the supplied NMEA sentence, adds a checksum (replacing any existing one) and pushes it out over the OpenCPN connections

OCPNpushNMEA(“$OCRMB,A,0.000,L,,Yarmouth,5030.530,N,00120.030,W,
       15.386,82.924,0.000,5030.530,S,00120.030,E,V");

A challenge has been how to arrange for the processing of OpenCPN events. I have developed a way of specifying a function to handle a particular event, so we can write, for example

OCPNonNMEAsentence(handleNMEA);

function handleNMEA(result){
    if(result.OK){
    print(“Received: “, result.value, “\n”);
        }
    else print(“Bad checksum\n”);
    }

This listens out for the next NMEA sentence and displays it on receipt if the checksum is OK.

Structures and methods

JavaScript supports the use of structures and methods. I have created structures to represent positions, waypoints and routes. You can write something like:

myposition = new Position(50.234, 3.231);
// let’s change the longitude
myposition.longitude = -1.674;
print(“My position is “, myPosition.formatted, ‘\n”);
// displays //My position is 50º 14.040’N 001º 40.440’W

mypos.NMEA returns the string 5014.04,N,140.44,W, which is the very odd way positions are represented in NMEA sentences.

This structure includes methods, as illustrated here:

sentence = "$OCRMB,A,0.000,L,,UK-S:Y,5030.530,N,00120.030,W,15.386,82.924,
0.000,5030.530,S,00120.030,E,V,A*69";
myPosition.NMEAdecode(sentence,1); // decodes NMEA sentence and sets
print(mypos.formatted, “\n"); //  displays 50º 30.530’N 001º 20.030’W
myPosition.latest(); // sets myPosition to the latest available from OpenCPN
print("My latest position is", myPosition.formatted, "\n");
// might display My latest position is 50º 23.50’N 001º 41.234’W

A simple application

As an illustrative application, I note that @arnolddemaa was having problems capturing magnetic variation from HDG sentences and inserting into RMC sentences. Here is a solution which captures the variation from HDG sentences and inserts it into any RMC sentences that do not already have the variation. It changes the RMC sender identity so that the originals can be blocked by OpenCPN filtering. It works by splitting the sentence into an array of fields. Note how it ends by setting up to process the next NMEA sentence.

// insert magnetic variation into RMC sentence
var vardegs = "";    // where we will save the latest variation
var varEW = "";
OCPNonNMEAsentence(processNMEA);

function processNMEA(result){
    if (result.OK){
        sentence = result.value;
        switch (sentence.slice(3,6)){
            case “HDG:"  // capture variation
                splut = sentence.split(",");
                vardegs = splut[4];    varEW = splut[5];
                break;
            case "RMC":
                splut = sentence.split(",");
                if (splut[10] == "") { // if no variation already
                    splut[10] = vardegs; splut[11] = varEW;
                    splut[0] = “$JSRMC”;
                        }
                result = splut.join(“,"); // put sentence together
                OCPNpushNMEA(result);
                break;
            }
        }
    OCPNonNMEAsentence(processNMEA);
    };

OpenCPN Messaging

I have implemented APIs to handle OpenCPN messages. Different messages can be directed to message-specific functions. For example:

//request route list
routeRequest = '{"mode": "Not track"}'   // JSON needed to get route
OCPNonMessageName(handleRL, “OCPN_ROUTELIST_RESPONSE”);
OCPNsendMessage("OCPN_ROUTELIST_REQUEST", routeRequest);

function handleRL(routeListJS){ //handle receipt of the route list
    routeList = JSON.parse(routeListJS);
    // notice how easy it is to parse the JSON into a structure
    // for illustration, here we extract the GUID of the first route
    firstGUID = routeList[0].GUID;
    }

Probing OpenCPN

I have found the plugin an excellent way of probing OpenCPN functionality, particularly as I can evolve the script in the light of what I get, iteratively. Wondering what a route list looks like? This will show you:

OCPNonMessageName(handleRL, “OCPN_ROUTELIST_RESPONSE”);
OCPNsendMessage("OCPN_ROUTELIST_REQUEST", JSON.stringify({"mode": "Not track”}));

function handleRL(routeListJS){  // handle route list response
      print(routeListJS, “\n”);
    }

A more complex application

My own particular interest has been to:

I have described that need in another post here. An advantage of iNavX is that it displays navigational information, including estimated times en-route, in a way optimized for iPad or small phone screens. The requirement is that

The first step is to obtain the active route GUID. It turns out there is a bug in OpenCPN v5 whereby the returned GUID is actually the route name and not the GUID. This has been fixed in OpenCPN v5.1.605. So my script checks to see whether the route name was returned instead of the GUID and, if so, it has to fetch the full list of routes and look to see which is the active one. This extra work is avoided if the GUID is returned correctly.

I have a script that works as I require. It is too long to include in this post but it can be viewed here. This script includes many comments to aid understanding and I have left in, but commented out, some of the print statements used during its development. This shows key points in its evolution .

Road map for future development

So far, I have only implemented the APIs needed to achieve my application above. I would like to hear of other possible applications so I can consider further APIs. The plugin is not yet ready for alpha testing but I would be interested to work with others in a pre-alpha phase to develop ideas. This pre-alpha phase will be MacOS only as I use Xcode for debugging. I propose we use a Slack workspace I set up to liaise with Mike. If you would like to join in, please contact me by private message. I anticipate developments will include:

At present, if you want to do separate tasks, you would need to combine them into a single script. I have ideas about running multiple independent scripts. I do not use SignalK but note its potential. I am interested in input from SignalK users to keep developments SignalK friendly.

Other suggestions?

Now to catch up on other things neglected over the past six weeks!

My JavaScript plugin is now available as an alpha release v0.1. The user guide is available here and an install package for MacOS can be downloaded from here.

I hope for feedback from users and am willing to implement further APIs to extend the interoperability with OpenCPN according to your interests.

I am Apple-based only, so am looking for volunteers to work with me on building the plugin for other platforms, especially Windows. As the present build uses Mac Xcode, someone with both Xcode and Windows would be ideal but we could probably manage if you are familiar with Cmake and the plugin development environment.

Development plan update

At present, if you want to do separate tasks, you would need to combine them into a single script. I have ideas about running multiple independent scripts.

I do not use SignalK but note its potential. I am interested in input from SignalK users to keep developments SignalK friendly.

A simple application

As an illustrative application, I note that @arnolddemaa was having problems capturing magnetic variation from HDG sentences and inserting into RMC sentences. Here is a solution which captures the variation from HDG sentences and inserts it into any RMC sentences that do not already have the variation. It changes the RMC sender identity so that the originals can be blocked by OpenCPN filtering. It works by splitting the sentence into an array of fields. Note how it ends by setting up to process the next NMEA sentence.

// insert magnetic variation into RMC sentence
var vardegs = "";	// where we will save the latest variation
var varEW = "";
OCPNonNMEAsentence(processNMEA);

function processNMEA(result){
	if (result.OK){
		sentence = result.value;
		switch (sentence.slice(3,6)){
			case “HDG:"  // capture variation
				splut = sentence.split(",");
				vardegs = splut[4];	varEW = splut[5];
				break;
			case "RMC":
				splut = sentence.split(",");
				if (splut[10] == "") { // if no variation already
					splut[10] = vardegs; splut[11] = varEW;
					splut[0] = “$JSRMC”;
				        }
				result = splut.join(“,"); // put sentence together
				OCPNpushNMEA(result);
				break;
			}
		}
	OCPNonNMEAsentence(processNMEA);
	};